﻿using System;
using System.Collections.Generic;
using System.Windows;
using System.Windows.Media;
using System.Windows.Shapes;
using Red.Controls.Curve.ViewModels;
using Red.Controls.TimeLine.Core;
using Red.Core;

namespace Red.Controls.Curve
{
    public abstract class CurveSegment : ViewModel, IDisposable
    {
        #region Data

        private ControlPoint _end;
        private ControlPoint _start;
        protected UIElement _adornedElement;

        #endregion Data

        #region Properties
        public ControlPoint Start
        {
            get { return _start; }
            set
            {
                if ( value != _start )
                {
                    if ( _start != null )
                    {
                        _start.PropertyChanged -= Start_PropertyChanged;
                        _start.PropertyChanging -= Start_PropertyChanging;
                    }

                    _start = value;

                    if ( _start != null )
                    {
                        _start.PropertyChanged += Start_PropertyChanged;
                        _start.PropertyChanging += Start_PropertyChanging;
                    }

                    OnPropertyChanged();
                }
            }
        }

        public ControlPoint End
        {
            get { return _end; }
            set
            {
                if ( value != _end )
                {
                    if ( _end != null )
                    {
                        _end.PropertyChanged -= End_PropertyChanged;
                        _end.PropertyChanging -= End_PropertyChanging;
                    }
                    OnPropertyChanging();

                    _end = value;

                    if ( _end != null )
                    {
                        _end.PropertyChanged += End_PropertyChanged;
                        _end.PropertyChanging += End_PropertyChanging;
                    }
                    OnPropertyChanged();
                }
            }
        }

        public TimeRuler HorizontalRuler { get; protected set; }
        public TimeRuler VerticalRuler { get; protected set; }

        public abstract List<Shape> VisualElement { get; }

        #endregion Properties

        public CurveSegment( ControlPoint start, ControlPoint end, TimeRuler horizontalRuler, TimeRuler verticalRuler, UIElement adornedElement )
        {
            _adornedElement = adornedElement;

            Start = start;
            End = end;
            HorizontalRuler = horizontalRuler;
            VerticalRuler = verticalRuler;

            PropertyChanged += CurveSegment_PropertyChanged;
        }

        #region Public methods
        public abstract void Arrange( double controlWidth, double controlHeight );

        #endregion Public methods

        #region Event handlers

        private void CurveSegment_PropertyChanged( object sender, System.ComponentModel.PropertyChangedEventArgs e )
        {
            if ( e.PropertyName == "Start" || e.PropertyName == "End" )
            {
                Arrange( _adornedElement.RenderSize.Width, _adornedElement.RenderSize.Height );
            }
        }

        protected virtual void Start_PropertyChanged( object sender, System.ComponentModel.PropertyChangedEventArgs e )
        {
            if ( e.PropertyName == "Time" || e.PropertyName == "Value" )
            {
                Arrange( _adornedElement.RenderSize.Width, _adornedElement.RenderSize.Height );
            }
        }

        protected virtual void Start_PropertyChanging( object sender, System.ComponentModel.PropertyChangingEventArgs e ) { }
        protected virtual void End_PropertyChanged( object sender, System.ComponentModel.PropertyChangedEventArgs e )
        {
            if ( e.PropertyName == "Time" || e.PropertyName == "Value" )
            {
                Arrange( _adornedElement.RenderSize.Width, _adornedElement.RenderSize.Height );
            }
        }

        protected virtual void End_PropertyChanging( object sender, System.ComponentModel.PropertyChangingEventArgs e ) { }
        #endregion Event handlers

        #region Protected methods
        protected double CalculateHorizontalOffset( double time )
        {
            return ( time - HorizontalRuler.StartValue ) * HorizontalRuler.PixelToValueRatio;
        }

        protected double CalculateVerticalOffset( double value )
        {
            return ( VerticalRuler.CurrentEndValue - value ) * VerticalRuler.PixelToValueRatio;
        }

        public virtual void Dispose()
        {
            PropertyChanged -= CurveSegment_PropertyChanged;
            Start = null;
            End = null;
        }
        #endregion Protected methods
    }

    public class ConstCurveSegment : CurveSegment
    {
        #region Data

        private Line _visual;

        // store the connector to next point in the segment itself
        private LinearCurveSegment _connector;

        #endregion Data

        #region Properties
        public override List<Shape> VisualElement
        {
            get
            {
                List<Shape> res = new List<Shape>();
                res.Add( _visual );
                res.AddRange( _connector.VisualElement );
                return res;
            }
        }

        #endregion Properties

        #region C-tor
        public ConstCurveSegment( ControlPoint start, ControlPoint end, Brush color, TimeRuler horizontalRuler, TimeRuler verticalRuler, UIElement adornedElement )
            : base( start, end, horizontalRuler, verticalRuler, adornedElement )
        {
            PropertyChanged += ConstCurveSegment_PropertyChanged;

            _visual = new Line();
            _visual.Stroke = color;
            _visual.StrokeThickness = 2;

            _connector = new LinearCurveSegment( new ControlPoint( end.Time, start.Value, EInteroplationType.CPT_Fake ), 
                end, color, HorizontalRuler, VerticalRuler, adornedElement, 2, true );
        }
        #endregion C-tor

        #region Public methods
        public override void Arrange( double controlWidth, double controlHeight )
        {
            if ( !HorizontalRuler.IsValueVisibleOnScreen( Start.Time ) && !HorizontalRuler.IsValueVisibleOnScreen( End.Time )
                && Math.Sign( HorizontalRuler.StartValue - Start.Time ) == Math.Sign( HorizontalRuler.StartValue - End.Time ) )
            {
                _visual.Visibility = Visibility.Hidden;
                return;
            }

            _visual.Visibility = Visibility.Visible;
            _visual.X1 = CalculateHorizontalOffset( Start.Time );
            _visual.X2 = CalculateHorizontalOffset( End.Time );
            _visual.Y1 = _visual.Y2 = CalculateVerticalOffset( Start.Value );

            _visual.Arrange( new Rect( 0, 0, controlWidth, controlHeight ) );
            _connector.Arrange( controlWidth, controlHeight );
        }

        #endregion Public methods

        #region Event handlers
        protected override void Start_PropertyChanged( object sender, System.ComponentModel.PropertyChangedEventArgs e )
        {
            base.Start_PropertyChanged( sender, e );

            if ( e.PropertyName == "Value" )
            {
                _connector.Start = new ControlPoint( End.Time, Start.Value, EInteroplationType.CPT_Fake );
            }
        }

        protected override void End_PropertyChanged( object sender, System.ComponentModel.PropertyChangedEventArgs e )
        {
            base.End_PropertyChanged( sender, e );

            if ( e.PropertyName == "Time" )
            {
                _connector.Start = new ControlPoint( End.Time, Start.Value, EInteroplationType.CPT_Fake );
            }
        }

        private void ConstCurveSegment_PropertyChanged( object sender, System.ComponentModel.PropertyChangedEventArgs e )
        {
            if ( e.PropertyName == "Start" )
            {
                if ( _connector != null )
                {
                    _connector.Start = new ControlPoint( End.Time, Start.Value, EInteroplationType.CPT_Fake );
                }
            }
            else if ( e.PropertyName == "End" )
            {
                if ( _connector != null )
                {
                    _connector.Start = new ControlPoint( End.Time, Start.Value, EInteroplationType.CPT_Fake );
                    _connector.End = End;
                }
            }
        }
        public override void Dispose()
        {
            PropertyChanged -= ConstCurveSegment_PropertyChanged;
            base.Dispose();
        }

        #endregion Event handlers
    }

    public class LinearCurveSegment : CurveSegment
    {
        #region Data

        private Line _visual;

        #endregion Data

        #region Properties
        public override List<Shape> VisualElement
        {
            get { return new List<Shape>() { _visual }; }
        }

        #endregion Properties

        #region C-tor
        public LinearCurveSegment( ControlPoint start, ControlPoint end, Brush color, TimeRuler horizontalRuler, TimeRuler verticalRuler, 
            UIElement adornedElement, double thickness = 2.0, bool dashed = false )
            : base( start, end, horizontalRuler, verticalRuler, adornedElement )
        {
            _visual = new Line();
            _visual.Stroke = color;
            _visual.StrokeThickness = 2;
            if ( dashed )
            {
                _visual.StrokeDashArray = new DoubleCollection( new double[] { 4, 3 } );
            }
        }

        #endregion C-tor

        #region Public methods

        public override void Arrange( double controlWidth, double controlHeight )
        {
            if ( ( !HorizontalRuler.IsValueVisibleOnScreen( Start.Time ) && !HorizontalRuler.IsValueVisibleOnScreen( End.Time ) ) 
                && Math.Sign( HorizontalRuler.StartValue - Start.Time ) == Math.Sign( HorizontalRuler.StartValue - End.Time ) )
            {
                _visual.Visibility = Visibility.Hidden;
                return;
            }

            _visual.Visibility = Visibility.Visible;
            _visual.X1 = CalculateHorizontalOffset( Start.Time );
            _visual.X2 = CalculateHorizontalOffset( End.Time );
            _visual.Y1 = CalculateVerticalOffset( Start.Value );
            _visual.Y2 = CalculateVerticalOffset( End.Value );

            _visual.Arrange( new Rect( 0, 0, controlWidth, controlHeight ) );
        }

        #endregion Public methods
    }

    public class BezierQuadraticSegment : CurveSegment
    {
        #region Data

        protected LinearCurveSegment _connectorStart;
        protected Path _path;

        protected Point? _cachedStart;
        protected Point? _cachedEnd;

        private ControlPoint _ctrlPointStart;

        #endregion Data

        #region Properties
        public ControlPoint CtrlPointStart
        {
            get { return _ctrlPointStart; }
            set
            {
                if ( value != _ctrlPointStart )
                {
                    if ( _ctrlPointStart != null )
                    {
                        _ctrlPointStart.PropertyChanged -= ControlPoint_PropertyChanged;
                    }

                    _ctrlPointStart = value;

                    if ( _ctrlPointStart != null )
                    {
                        _ctrlPointStart.PropertyChanged += ControlPoint_PropertyChanged;
                    }

                    OnPropertyChanged();
                }
            }
        }

        public override List<Shape> VisualElement
        {
            get
            {
                List<Shape> res = new List<Shape>();
                res.Add( _path );
                res.AddRange( _connectorStart.VisualElement );
                return res;
            }
        }

        #endregion Properties

        #region C-tor
        public BezierQuadraticSegment( ControlPoint start, ControlPoint end, ControlPoint ctrlPoint, Brush color, 
            TimeRuler horizontalRuler, TimeRuler verticalRuler, UIElement adornedElement )
            : base( start, end, horizontalRuler, verticalRuler, adornedElement )
        {
            PathFigure fig = new PathFigure( start.Coordinates, new List<PathSegment>() { 
                new QuadraticBezierSegment( ctrlPoint.Coordinates, end.Coordinates, true ) }, false );
            PathGeometry geom = new PathGeometry( new List<PathFigure>() { fig } );
            
            _path = new Path();
            _path.Data = geom;
            _path.Stroke = color;
            _path.StrokeThickness = 2;

            _connectorStart = new LinearCurveSegment( start, ctrlPoint, color, HorizontalRuler, VerticalRuler, adornedElement, 1, true );

            CtrlPointStart = ctrlPoint;

            PropertyChanged += BezierQuadraticSegment_PropertyChanged;
            PropertyChanging += BezierQuadraticSegment_PropertyChanging;
        }

        #endregion C-tor

        #region Public methods
        public override void Arrange( double controlWidth, double controlHeight )
        {
            if ( ( !HorizontalRuler.IsValueVisibleOnScreen( Start.Time ) && !HorizontalRuler.IsValueVisibleOnScreen( End.Time ) )
                && Math.Sign( HorizontalRuler.StartValue - Start.Time ) == Math.Sign( HorizontalRuler.StartValue - End.Time ) )
            {
                _path.Visibility = Visibility.Hidden;
                return;
            }

            _path.Visibility = Visibility.Visible;

            double startTime = CalculateHorizontalOffset( Start.Time );
            double controlPointTime = CalculateHorizontalOffset( CtrlPointStart.Time );
            double endTime = CalculateHorizontalOffset( End.Time );

            ( _path.Data as PathGeometry ).Figures[0].StartPoint = new Point( startTime, CalculateVerticalOffset( Start.Value ) );
            ( _path.Data as PathGeometry ).Figures[0].Segments[0] = new QuadraticBezierSegment(
                new Point( controlPointTime, CalculateVerticalOffset( CtrlPointStart.Value ) ),
                new Point( endTime, CalculateVerticalOffset( End.Value ) ), true );

            _path.Arrange( new Rect( 0, 0, controlWidth, controlHeight ) );
            _connectorStart.Arrange( controlWidth, controlHeight );
        }

        #endregion Public methods

        #region Event handlers

        private void BezierQuadraticSegment_PropertyChanging( object sender, System.ComponentModel.PropertyChangingEventArgs e )
        {
            if ( e.PropertyName == "End" )
            {
                CacheEndPoint();
            }
        }

        private void BezierQuadraticSegment_PropertyChanged( object sender, System.ComponentModel.PropertyChangedEventArgs e )
        {
            if ( e.PropertyName == "Start" )
            {
                if ( _connectorStart != null )
                {
                    _connectorStart.Start = Start;
                }
            }
            else if ( e.PropertyName == "CtrlPointStart" )
            {
                Arrange( _adornedElement.RenderSize.Width, _adornedElement.RenderSize.Height );
            }
        }

        protected void ControlPoint_PropertyChanged( object sender, System.ComponentModel.PropertyChangedEventArgs e )
        {
            if ( e.PropertyName == "Time" || e.PropertyName == "Value" )
            {
                Arrange( _adornedElement.RenderSize.Width, _adornedElement.RenderSize.Height );
            }
        }

        protected override void Start_PropertyChanging( object sender, System.ComponentModel.PropertyChangingEventArgs e )
        {
            base.Start_PropertyChanging( sender, e );

            if ( e.PropertyName == "Value" || e.PropertyName == "Time" )
            {
                if ( Start != null )
                {
                    _cachedStart = new Point( Start.Time, Start.Value );
                }
                else
                {
                    _cachedStart = null;
                }
            }
        }

        protected override void Start_PropertyChanged( object sender, System.ComponentModel.PropertyChangedEventArgs e )
        {
            base.Start_PropertyChanged( sender, e );

            if ( e.PropertyName == "Value" || e.PropertyName == "Time" )
            {
                if ( _cachedStart.HasValue )
                {
                    Vector delta = Start.Coordinates - _cachedStart.Value;
                    Point newCoords = CtrlPointStart.Coordinates + delta;

                    if ( newCoords.X > End.Time )
                    {
                        CtrlPointStart.Value = newCoords.Y;
                        Start.Time = _cachedStart.Value.X;
                    }
                    else
                    {
                        CtrlPointStart.Coordinates = newCoords;
                    }
                }
            }
        }

        #endregion Event handlers

        #region Protected methods
        protected void CacheEndPoint()
        {
            if ( End != null )
            {
                _cachedEnd = new Point( End.Time, End.Value );
            }
            else
            {
                _cachedEnd = null;
            }
        }
        public override void Dispose()
        {
            PropertyChanged -= BezierQuadraticSegment_PropertyChanged;
            PropertyChanging -= BezierQuadraticSegment_PropertyChanging;
            CtrlPointStart = null;

            base.Dispose();
        }

        #endregion Protected emthods
    }

    public class BezierCubicSegment : BezierQuadraticSegment
    {
        #region Data

        private bool _updating = false;
        private LinearCurveSegment _connectorEnd;
        private ControlPoint _ctrlPointEnd;

        #endregion Data

        #region Properties
        public ControlPoint CtrlPointEnd
        {
            get { return _ctrlPointEnd; }
            set
            {
                if ( value != _ctrlPointEnd )
                {
                    if ( _ctrlPointEnd != null )
                    {
                        _ctrlPointEnd.PropertyChanged -= ControlPoint_PropertyChanged;
                    }

                    _ctrlPointEnd = value;

                    if ( _ctrlPointEnd != null )
                    {
                        _ctrlPointEnd.PropertyChanged += ControlPoint_PropertyChanged;
                    }

                    OnPropertyChanged();
                }
            }
        }

        public override List<Shape> VisualElement
        {
            get
            {
                List<Shape> res = new List<Shape>();
                res.AddRange( base.VisualElement );
                res.AddRange( _connectorEnd.VisualElement );
                return res;
            }
        }

        #endregion Properties

        #region C-tor
        public BezierCubicSegment( ControlPoint start, ControlPoint end, ControlPoint ctrlPoint1, ControlPoint ctrlPoint2, Brush color, 
            TimeRuler horizontalRuler, TimeRuler verticalRuler, UIElement adornedElement )
            : base( start, end, ctrlPoint1, color, horizontalRuler, verticalRuler, adornedElement )
        {
            PathFigure fig = new PathFigure( start.Coordinates, 
                new List<PathSegment>() { new BezierSegment( ctrlPoint1.Coordinates, ctrlPoint2.Coordinates, end.Coordinates, true ) }, false );
            PathGeometry geom = new PathGeometry( new List<PathFigure>() { fig } );

            _path.Data = geom;
            _connectorEnd = new LinearCurveSegment( end as ControlPoint, ctrlPoint2, color, HorizontalRuler, VerticalRuler, adornedElement, 1, true );

            CtrlPointEnd = ctrlPoint2;
            PropertyChanged += BezierCubicSegment_PropertyChanged;
        }

        #endregion C-tor

        #region Public methods
        public override void Arrange( double controlWidth, double controlHeight )
        {
            if ( ( !HorizontalRuler.IsValueVisibleOnScreen( Start.Time ) && !HorizontalRuler.IsValueVisibleOnScreen( End.Time ) )
                && Math.Sign( HorizontalRuler.StartValue - Start.Time ) == Math.Sign( HorizontalRuler.StartValue - End.Time ) )
            {
                _path.Visibility = Visibility.Hidden;
                return;
            }

            double startTime = CalculateHorizontalOffset( Start.Time );
            double controlPointTime1 = CalculateHorizontalOffset( CtrlPointStart.Time );
            double controlPointTime2 = CalculateHorizontalOffset( CtrlPointEnd.Time );
            double endTime = CalculateHorizontalOffset( End.Time );

            ( _path.Data as PathGeometry ).Figures[0].StartPoint = new Point( startTime, CalculateVerticalOffset( Start.Value ) );
            ( _path.Data as PathGeometry ).Figures[0].Segments[0] = new BezierSegment(
                new Point( controlPointTime1, CalculateVerticalOffset( CtrlPointStart.Value ) ),
                new Point( controlPointTime2, CalculateVerticalOffset( CtrlPointEnd.Value ) ),
                new Point( endTime, CalculateVerticalOffset( End.Value ) ), true );

            _path.Arrange( new Rect( 0, 0, controlWidth, controlHeight ) );
            _connectorStart.Arrange( controlWidth, controlHeight );
            _connectorEnd.Arrange( controlWidth, controlHeight );
        }

        #endregion Public methods

        #region Event handlers
        private void BezierCubicSegment_PropertyChanged( object sender, System.ComponentModel.PropertyChangedEventArgs e )
        {
            if ( e.PropertyName == "End" )
            {
                if ( _connectorEnd != null )
                {
                    _connectorEnd.Start = End;
                }
            }
            else if ( e.PropertyName == "CtrlPointEnd" )
            {
                Arrange( _adornedElement.RenderSize.Width, _adornedElement.RenderSize.Height );
            }
        }

        protected override void End_PropertyChanging( object sender, System.ComponentModel.PropertyChangingEventArgs e )
        {
            base.End_PropertyChanging( sender, e );

            if ( e.PropertyName == "Value" || e.PropertyName == "Time" )
            {
                CacheEndPoint();
            }
        }
        protected override void Start_PropertyChanged( object sender, System.ComponentModel.PropertyChangedEventArgs e )
        {
            if ( e.PropertyName == "Value" || e.PropertyName == "Time" )
            {
                if ( !_updating && _cachedStart.HasValue )
                {
                    Vector delta = Start.Coordinates - _cachedStart.Value;
                    Point newCoords = CtrlPointStart.Coordinates + delta;

					// if control points are about to change their order, block it
                    if ( newCoords.X > CtrlPointEnd.Time )
                    {
                        _updating = true;
                        CtrlPointStart.Value = newCoords.Y;
                        Start.Time = _cachedStart.Value.X;
                        _updating = false;
                    }
                    else
                    {
                        CtrlPointStart.Coordinates = newCoords;
                    }
                }
                Arrange( _adornedElement.RenderSize.Width, _adornedElement.RenderSize.Height );
            }
        }

        protected override void End_PropertyChanged( object sender, System.ComponentModel.PropertyChangedEventArgs e )
        {
            base.End_PropertyChanged( sender, e );

            if ( e.PropertyName == "Value" || e.PropertyName == "Time" )
            {
                UpdateEndPoint();
            }
        }

        #endregion Event handlers

        #region Protected methods
        protected void UpdateEndPoint()
        {
            if ( _cachedEnd.HasValue && End != null )
            {
                Vector delta = End.Coordinates - _cachedEnd.Value;
                Point newCoords = CtrlPointEnd.Coordinates + delta;

				// if control points are about to change their order, block it
                if ( newCoords.X < CtrlPointStart.Time )
                {
                    CtrlPointEnd.Value = newCoords.Y;
                    End.Coordinates = _cachedEnd.Value;
                }
                else
                {
                    CtrlPointEnd.Coordinates = newCoords;
                }
            }
        }

        public override void Dispose()
        {
            PropertyChanged -= BezierCubicSegment_PropertyChanged;
            CtrlPointEnd = null;
            base.Dispose();
        }

        #endregion Protected methods
    }
}
